设计模式与原则

# 设计模式与原则

参考教程

常用设计模式在前端开发中的应用 (opens new window)

前端渣渣唠嗑一下前端中的设计模式(真实场景例子) (opens new window)

[TOC]

# 一、预备知识

# 1.1 开发环境

// 淘宝镜像:taobao.org上找
npm init
npm install webpack webpack-cli --save-dev
npm install webpack-dev-server html-webpack-plugin --save-dev
npm install babel-core babel-loader babel-polyfill babel-preset-es2015 babel-preset-latest --save-dev
1
2
3
4
5
// webpack.dev.config.js

// CommomJS规范下的引入模板
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/index.js',

  output: {
    // 路径,当前文件夹目录下
    path: __dirname,
    filename: './release/bundle.js'
  },

  module: {
    rules: [{
      // 匹配js文件
      test: /\.js$/,
      // 排除对node_modules文件夹的文件匹配
      exclude: /(node_modules)/,
      // 用babel-loader插件将es6转换为es5
      loader: 'babel-loader'
    }]
  },

  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html'
    })
  ],
  
  devServer: {
    // 本地开发环境服务器
    // 源文件修改后,服务器会自动刷新
    // 根目录
    contentBase: path.join(__dirname, './release'),
    // 自动打开浏览器
    open: true,
    port: 9000
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// .babelrc
{
    "presets": [
        "es2015",
        "latest"
    ],
    "plugins": []
}
1
2
3
4
5
6
7
8
  • 其他
$ npm install http-server -g
$ http-server -p 8881
localhost://8881/test.html
1
2
3

# 1.1.2 UML类图

Unified Modeling Language 统一建模语言

  • 画图工具:https://www.processon.com
  • 关系
    • 泛化,表示继承 - 空心箭头
    • 关联,表示引用 - 实心箭头
  • 题目

第一题

打车时,可以打专车或者快车。任何车都有车牌号和名称。 不同车价格不同,快车每公里1元,专车每公里2元。 行程开始时,显示车辆信息。 行程结束时,显示打车金额(假定行程就5公里)。

car-uml

class Car {
  constructor(number, name) {
    this.number = number;
    this.name = name;
  }
}

class Kuaiche extends Car {
  constructor(number, name) {
    super(number, name);
    this.price = 1;
  }
}

class Zhuanche extends Car {
  constructor(number, name) {
    super(number, name);
    this.price = 2;
  }
}

class Trip {
  constructor(car) {
    this.car = car;
  }
  start() {
    console.log(`trip start: name-${this.car.name}, number-${this.car.number}`);
  }
  end() {
    console.log(`trip end: price-${this.car.price * 5}`);
  }
}

let car = new Kuaiche(1200,'daben');
let trip = new Trip(car);
trip.start();
trip.end();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

第二题

某停车场,分3层,每层100车位。 每个车位都能监控到车辆的驶入和离开。 车辆进入前,显示每层的空余车位数量。 车辆进入时,摄像头可识别车牌号和时间。 车辆出来时,出口显示器显示车牌号和停车时长。

park-uml

# 二、设计原则

必看的书籍:《UNIX/LINUX设计哲学》

  • 准则:
    • 1:小即是美
    • 2:让每个程序只做好一件事
    • 3:快速建立原型
    • 4:舍弃好效率而取可移植性
    • 5:采用纯文本来存储数据
    • 6:充分利用软件的杠杆效应(软件复用)
    • 7:使用shell脚本来提高杠杆效应和可移植性
    • 8:避免强制性的用户界面
    • 9:让每个程序都成为过滤器
  • 小准则:
    • 1:允许用户定制环境
    • 2:尽量使操作系统内核小而轻量化
    • 3:使用小写字母并尽量简短
    • 4:沉默是金
    • 5:各部分之和大于整体
    • 6:寻求90%的解决方案

# 2.1 SOLID 五大设计原则

# 2.1.1 S - 单一职责原则*

  • 一个程序只做好一件事。
  • 如果功能过于复杂就拆分开,每个部分保持独立。

# 2.1.2 O - 开放封闭原则*

  • 对扩展开发,对修改封闭。
  • 增加需求时,扩展新代码,而非修改已有代码。
  • 这是软件设计的终极目标。
// 用Promise来说明S O
// S: 每个 then 中的逻辑只做好一件事
// O:如果新增需求,扩展then即可

// 加载图片
function loadImg(src) {
    return new Promise(function (resolve, reject) {
        let img = document.createElement('img');
        img.onload = ()=>resolve(img);
        img.onerror = ()=>reject('load img fail');
        img.src = src;
    });
}

let result = loadImg('XXX.png');
result.then((img)=>img)
    .then((img)=>console.log(img))
    .catch((error)=>console.log(error))	// 统一捕获异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.1.3 L - 李氏置换原则

  • 子类能覆盖父类。
  • 父类能出现的地方,子类就能出现。
  • JS中使用较少(弱类型 & 继承使用较少)。

# 2.1.4 I - 接口独立原则

  • 保持接口的单一独立,避免出现”胖接口“。
  • JS中没有接口(ts例外),使用较少。
  • 类似于单一职责原则,这里更关注接口。

# 2.1.5 D - 依赖倒置原则

  • 面向接口编程,依赖于抽象而不依赖于具体。
  • 使用方只关注接口而不关注具体类的实现。
  • JS中使用较少(没有接口&弱类型)。

# 三、模式

# 3.1 创建型

# 3.1.1 工厂模式*

  • 介绍
    • 将new操作单独封装。
    • 遇到new时,就要考虑是否该使用工厂模式。

factory-uml

class Product {
  constructor(name) {
    this.name = name;
  }
  fn1() {
    console.log('fn1');
  }
  fn2() {
    console.log('fn2');
  }
}

class Creator {
  create(name) {
    return new Product(name);
  }
}

let creator = new Creator();
let p = creator.create('p1');
p.fn1();
p.fn2();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 场景

    • JQuery: $(‘div')
    • React.createElement
    class Vnode(tag, attrs, children) {
        // ...
    }
    React.createElement = function (tag, attrs, children) {
        return new Vnode(tag, attrs, children)
    }
    
    1
    2
    3
    4
    5
    6
    • vue异步组件
  • 验证

    • 构造函数和创建者分离
    • 符合开放封闭原则

# 3.1.2 单例模式*

  • 介绍
    • 系统中被唯一使用。
    • 一个类只用一个实例。
class singleObj {
  login() {
    console.log('login');
  }
}

// 闭包
singleObj.getInstance = (function () {
  let instance;
  // console.log(this);// window
  return function () {
    // console.log(this);// singleObj
    return instance = instance || new singleObj();;
  }
})();

let obj1 = singleObj.getInstance();
let obj2 = singleObj.getInstance();
obj1 === obj2	// true

let obj3 = new singleObj();
obj1 === obj3	// false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 场景

    • jQuery 只有一个 $
    if (window.jQuery != null) {
        return window.jQuery;
    } else {
        // init...
    }
    
    1
    2
    3
    4
    5
    • 登录框

    • vuex 和 redux 中的 store

    • 创建唯一的浮窗

let getSingle = function( fn ){
   let result;
   return function(){
       return result || ( result = fn .apply(this, arguments ) );
   } 
};
let createLoginLayer = function(){
    let div = document.createElement( 'div' );
    div.innerHTML = '我是登录浮窗';
    document.body.appendChild( div );
    return div;
};

let createSingleLoginLayer = getSingle( createLoginLayer );
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.1.3 原型模式

  • clone自己,生成一个新对象。

  • 应用: Object.create

# 3.2 结构型

# 3.2.1 适配器模式*

  • 介绍
    • 旧接口格式和使用者不兼容。
    • 中间加一个适配转换接口。

adapter-uml

class Adaptee {
  oldRequest() {
    return 'old';
  }
}

class Target {
  constructor() {
    this.adaptee = new Adaptee();
  }
  request() {
    let info = this.adaptee.oldRequest();
    return `${info} - conver -> new`
  }
}

let target = new Target();
console.log(target.request());
// old - conver -> new
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 场景

    • 封装旧接口
// 自己封装的 Ajax
ajax({
    url:'/getData',
    type:'POST',
    dataType:'json',
    data:{
        id:"123"
    }
})
.done(function(){});

// 历史原因
$.ajax({
    //...
});
        
// 做一层适配器
let $ = {
    ajax: function (options) {
        return ajax(options);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • vue computed

# 3.2.2 装饰器模式

  • 介绍
    • 为对象添加新功能。
    • 不改变其原有的结构和功能。

decorator-mode

class Circle {
  draw() {
    console.log('draw a circle');
  }
}

class Decorator {
  constructor(circle) {
    this.circle = circle;
  }
  draw(){
    this.circle.draw();
    this.setColor(circle);
  }
  setColor(circle) {
    console.log(`add color to circle`);
  }
}

let circle = new Circle();
circle.draw();

let decorator = new Decorator(circle);
decorator.draw();

// draw a circle
// draw a circle
// add color to circle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 场景

    • ES7装饰器

      • 配置环境
      npm i babel-core babel-plugin-transform-decorators-legacy -D
      
      1
      • .babelrc
      "plugins":["transform-decorators-legacy"]
      
      1
// 修饰器: 编译时执行的函数
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;

// 修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升,而类不会提升。
1
2
3
4
5
6
7
8
// mixin模式:一个对象中混入另外一个对象的方法。(对象继承)
const Foo = {
    foo() { console.log('foo') }  
};
class MyClass {}
Object.assign(myClass.prototype, Foo);
let obj = new MyClass();
obj.foo();	// 'foo'


// 修饰器实现
// minxins.js
export function mixins(...list) {
    return function (target) {
        Object.assign(target.prototype, ...list);
    };
}

// index.js
import { mixins } from './mixins';
const Foo = {
    foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo();	// 'foo'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • core-decorators
// npm i core-decorators --save
import { readonly } from 'core-decorators';

class Meal {
  @readonly
  entree = 'steak';
}

let dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [Object Object]
1
2
3
4
5
6
7
8
9
10
11

# 3.2.3 代理模式*

  • 介绍
    • 使用者无权访问目标对象。
    • 中间加代理,通过代理做授权和控制。

proxy-mode

proxy-uml

class RealImg {
    constructor(fileName) {
        this.fileName = fileName;
    }
    display() {
        console.log('display ' + this.fileName)
    }
}

class ProxyImg {
    constructor(fileName) {
        this.realImg = new RealImg(fileName);
    }
    display() {
        this.realImg.display();
    }
}

let proxyImg = new ProxyImg('1.png');
proxyImg.display();

// display 1.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 场景

    • 网页事件代理 - 事件委托
    <div id="div1">
        <a href="#">a1</a>
        <a href="#">a2</a>
    </div>
    
    <script>
    	let div1 = document.getElementById('div1');
        div1.addEventListener('click', function(e) {
            let target = e.target;
            if (target.nodeName === 'A') {
                console.log(target.innerHTML);
            }
        });
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    • jQuery $.proxy
    // 常用
    $('#div1').click(function() {
        let _this = this;
        setTimoeout(function() {
            _this.css('background-color','yellow');
        }, 1000);
    });
    
    // 箭头函数
    $('#div1').click(function() {
        setTimoeout(() => {
            this.css('background-color','yellow');
        }, 1000);
    });
    
    // JQuery
    $('#div1').click(function() {
        setTimoeout($.proxy(function() {
            _this.css('background-color','yellow');
        }, this), 1000);
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    • ES6 proxy
    // 明星
    let star = {
        name: 'Lin',
        age: 22,
        phone: '10086'
    };
    
    // 经纪人
    let agent = new Proxy(star, {
        get: function (target, key) {
            if (key === 'phone') {
                return '10000';
            }
            if (key === 'price') {
                return '120000RMB';
            }
            return target[key];
        },
        set: function (target, key, val) {
            if (key === 'customPrice') {
                if (val < 120000) {
                    throw new Error('too low');
                } else {
                    target[key] = val;
                    return true;
                }
            }
        }
    })
    
    console.log(agent.name);  // Lin
    console.log(agent.age);   // 22
    console.log(agent.phone); // 10000
    console.log(agent.price); // 120000RMB
    
    agent.customPrice = 150000;
    console.log(agent.customPrice); // 150000
    agent.customPrice = 100000;
    console.log(agent.customPrice); // Error: too low
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
# 3.2.3.1 代理模式 VS 适配器模式
  • 适配器模式:提供一个不同的接口(如不同版本的插头)。

  • 代理模式:提供一摸一样的接口。

# 3.2.3.2 代理模式 VS 装饰器模式
  • 装饰器模式:扩展功能,原有功能不变且可直接使用。
  • 代理模式:显示原有功能,但是经过限制或者阉割之后的。

# 3.2.4 外观模式*

  • 介绍
    • 为子系统中的一组接口提供了一个高层接口。
    • 使用者使用这个高层接口。

facade-mode

facade-uml

  • 验证
    • 不符合单一职责原则和开放封闭原则。
    • 谨慎使用,不可滥用。
    • 优点:使用者不需要理解内部实现逻辑,主要用于第三方库。

# 3.2.5 其他模式

# 3.2.5.1 桥接模式
  • 用于把抽象化与实现化解耦,使二者可以独立变化。
# 3.2.5.2 组合模式
  • 生成树形结构,表示“整体-部分”关系。
  • 让整体和部分都具有一致的操作方式。
# 3.2.5.3 享元模式
  • 共享内存(主要考虑内存,而非效率)。
  • 相同的数据,共享使用。

# 3.3 行为型

# 3.3.1 发布订阅(观察者)模式*

  • 介绍
    • 发布 & 订阅。
    • 一对多。
class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(...sub);
    }
    notify(value) {
        this.subs.forEach(v => {
            v.update(value);
        });
    }
}

class Watch {
    constructor(name) {
        this.name = name;
    }
    update(value) {
        console.log(`${this.name} ${value}`);
    }
}

let dep = new Dep();
let w1 = new Watch('w1');
let w2 = new Watch('w2');
dep.addSub([w1, w2]);
dep.notify('updated');
// w1 updated
// w2 updated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  • 场景

    • 网页事件绑定
    document.body.addEventListener('click',function(){
        alert(1);
    });
    document.body.addEventListener('click',function(){
        alert(2);
    });
    document.body.addEventListener('click',function(){
        alert(3);
    });
    // 模拟用户点击
    document.body.click();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    • Promise
    promise.then().then()
    
    1
    • jQuery callbacks
    • nodejs 自定义事件
    • nodejs 中:处理http请求;多进程通讯
    • vue 和 React 组件生命周期触发
    • vue watch

# 3.3.2 迭代器模式*

  • 介绍
    • 顺序访问一个集合。
    • 使用者无需知道集合的内部结构(封装)。

lterator

class Iterator {
    constructor(container) {
        this.list = container.list;
        this.index = 0;
    }
    next() {
        return this.index < this.list.length ? {
            value: this.list[this.index++],
            done: false
        } : {
            value: undefined,
            done: true
        };
    }
}

class Container {
    constructor(list) {
        this.list = list;
    }
    // 生成遍历器
    getIterator() {
        return new Iterator(this);
    }
}

let container = new Container(['a', 'b', 'c']);
let iterator = container.getIterator();
let res = {};
do {
    res = iterator.next();
    console.log(res);
} while (!res.done);

// {value: 'a', done: false}
// {value: 'b', done: false}
// {value: 'c', done: false}
// {value: undefined, done: true}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  • 场景
    • ES6 Iterator

# 3.3.3 状态模式*

  • 介绍
    • 一个对象有状态变化。
    • 每次状态变化都会触发一个逻辑。
    • 不能总是用if-else来控制。

state-uml

class State {
    constructor(color) {
        this.color = color;
    }
    handle(context) {
        console.log(`turn ${this.color}`);
        context.setState(this);
    }
}

class Context {
    constructor() {
        this.state = null;
    }
    getState() {
        return this.state;
    }
    setState(state) {
        this.state = state;
    }
}

let context = new Context();

let green = new State('green');
let yellow = new State('yellow');

green.handle(context);	// turn green
context.getState();		// State {color: "green"}
yellow.handle(context); // turn yellow
context.getState();		// State {color: "yellow"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  • 场景
    • 有限状态机

# 3.3.4 其他模式

# 3.3.4.1 模板方法模式
# 3.3.4.2 职责模式
# 3.3.4.3 命令模式
  • 执行命令时,发布者和执行者分开。
  • 中间加入命令对象,作为中转站。
# 3.3.4.4 备忘录模式
  • 随时记录一个对象的状态变化。
  • 随时可以恢复之前的某个状态(如撤销功能)。
# 3.3.4.5 访问者模式
  • 将数据操作和数据结构分开。
# 3.3.4.6 中介者模式
# 3.3.4.7 解释器模式
  • 描述语言语法如何定义,如何解释和编译。
# 3.3.4.8 策略模式
  • 不同策略分开处理。
  • 避免出现大量if-else或者switch-else。